// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:kernel/kernel.dart';
import 'package:kernel/util/graph.dart' as kernel_graph;

/// Returns true iff the node has an `@JS(...)` annotation from `package:js`,
/// `dart:_js_annotations`, or `dart:_js_interop`. Note that while `package:js`
/// has no annotations any more, it used to, so we still need to support those
/// versions.
bool hasJSInteropAnnotation(Annotatable a) =>
    a.annotations.any(_isJSInteropAnnotation);

/// Returns true iff the node has an `@JS(...)` annotation from `package:js` or
/// `dart:_js_annotations`.
bool hasPackageJSAnnotation(Annotatable a) =>
    a.annotations.any(_isPackageJSAnnotation);

/// Returns true iff the node has an `@JS(...)` annotation from
/// `dart:js_interop`.
bool hasDartJSInteropAnnotation(Annotatable a) =>
    a.annotations.any(_isDartJSInteropAnnotation);

/// Returns true iff the node has an `@anonymous` annotation from `package:js`
/// or `dart:js_interop`.
bool hasAnonymousAnnotation(Annotatable a) =>
    a.annotations.any(_isAnonymousAnnotation);

/// Returns true iff the node has an `@staticInterop` annotation from
/// `package:js` or `dart:js_interop`.
bool hasStaticInteropAnnotation(Annotatable a) =>
    a.annotations.any(_isStaticInteropAnnotation);

/// Returns true iff the node has an `@trustTypes` annotation from `package:js`
/// or `dart:_js_annotations`.
bool hasTrustTypesAnnotation(Annotatable a) =>
    a.annotations.any(_isTrustTypesAnnotation);

/// Returns true iff the node has an `@JSExport(...)` annotation from
/// `package:js` or `dart:js_interop`.
bool hasJSExportAnnotation(Annotatable a) =>
    a.annotations.any(_isJSExportAnnotation);

/// Returns true iff the node has an `@Native(...)` annotation from the internal
/// `dart:_js_helper`.
bool hasNativeAnnotation(Annotatable a) =>
    a.annotations.any(_isNativeAnnotation);

/// Returns true iff the node has an `@patch` annotation from `dart:_internal`.
bool hasPatchAnnotation(Annotatable a) => a.annotations.any(_isPatchAnnotation);

/// If [a] has a `@JS('...')` annotation, returns the value inside the
/// parentheses.
///
/// This function only considers `JS` annotations defined in [interopLibraries]
/// or, when that argument is not set, from both `package:js` and
/// `dart:js_interop`.
///
/// If there is none or the class does not have a `@JS()` annotation, returns
/// an empty String.
String getJSName(Annotatable a, {Set<Uri>? interopLibraries}) {
  String jsClass = '';
  for (var annotation in a.annotations) {
    if (_isJSInteropAnnotation(
      annotation,
      interopLibraries: interopLibraries,
    )) {
      var jsClasses = stringAnnotationValues(annotation);
      if (jsClasses.isNotEmpty) {
        jsClass = jsClasses[0];
      }
    }
  }
  return jsClass;
}

/// If [a] has a `JS('...')` annotation from `dart:js_interop`, returns the
/// value inside the parentheses.
///
/// If no such annotation exists, returns an empty string.
String getDartJSInteropJSName(Annotatable a) {
  return getJSName(a, interopLibraries: {_jsInterop});
}

/// If [a] has a `@Native('...')` annotation, returns the values inside the
/// parentheses.
///
/// If there are none or the class does not have a `@Native()` annotation,
/// returns an empty list. Unlike `@JS()`, the string within `@Native()` is
/// allowed to contain several classes separated by a `,`.
List<String> getNativeNames(Annotatable a) {
  List<String> nativeClasses = [];
  for (var annotation in a.annotations) {
    if (_isNativeAnnotation(annotation)) {
      nativeClasses.addAll(stringAnnotationValues(annotation));
    }
  }
  return nativeClasses;
}

/// If [a] has a `@JSExport('...')` annotation, returns the value inside the
/// parentheses.
///
/// If there is no value or the class does not have a `@JSExport()` annotation,
/// returns an empty String.
String getJSExportName(Annotatable a) {
  String jsExportValue = '';
  for (var annotation in a.annotations) {
    if (_isJSExportAnnotation(annotation)) {
      var jsExportValues = stringAnnotationValues(annotation);
      // TODO(srujzs): Theoretically, this should never be empty as there is a
      // default empty value. However, in the modular tests, dart2js modular
      // analysis does not see the default value, and reports this as empty in
      // some cases. We should investigate why and fix it, but for now, we just
      // manually provide the default value.
      if (jsExportValues.isNotEmpty) {
        jsExportValue = jsExportValues[0];
      }
    }
  }
  return jsExportValue;
}

final _packageJs = Uri.parse('package:js/js.dart');
final _internal = Uri.parse('dart:_internal');
final _jsAnnotations = Uri.parse('dart:_js_annotations');
final _jsHelper = Uri.parse('dart:_js_helper');
final _jsInterop = Uri.parse('dart:js_interop');

/// Returns true if [value] is the interop annotation whose class is
/// [annotationClassName] from [interopLibraries].
///
/// If [interopLibraries] is null, we check `package:js`,
/// `dart:_js_annotations`, and `dart:js_interop`.
bool _isInteropAnnotation(
  Expression value,
  String annotationClassName, {
  Set<Uri>? interopLibraries,
}) {
  interopLibraries ??= {_packageJs, _jsAnnotations, _jsInterop};
  var c = annotationClass(value);
  if (c == null || c.name != annotationClassName) return false;
  var importUri = c.enclosingLibrary.importUri;
  return interopLibraries.contains(importUri);
}

bool _isJSInteropAnnotation(Expression value, {Set<Uri>? interopLibraries}) =>
    _isInteropAnnotation(value, 'JS', interopLibraries: interopLibraries);

bool _isPackageJSAnnotation(Expression value) => _isInteropAnnotation(
  value,
  'JS',
  interopLibraries: {_packageJs, _jsAnnotations},
);

bool _isDartJSInteropAnnotation(Expression value) =>
    _isInteropAnnotation(value, 'JS', interopLibraries: {_jsInterop});

bool _isAnonymousAnnotation(Expression value) =>
    _isInteropAnnotation(value, '_Anonymous');

bool _isStaticInteropAnnotation(Expression value) =>
    _isInteropAnnotation(value, '_StaticInterop');

bool _isTrustTypesAnnotation(Expression value) =>
    _isInteropAnnotation(value, '_TrustTypes');

bool _isJSExportAnnotation(Expression value) =>
    _isInteropAnnotation(value, 'JSExport');

/// Returns true if [value] is the `Native` annotation from `dart:_js_helper`.
bool _isNativeAnnotation(Expression value) {
  var c = annotationClass(value);
  return c != null &&
      c.name == 'Native' &&
      c.enclosingLibrary.importUri == _jsHelper;
}

/// Returns true if [value] is the `patch` annotation from `dart:_internal`.
bool _isPatchAnnotation(Expression value) {
  var c = annotationClass(value);
  return c != null &&
      c.name == '_Patch' &&
      c.enclosingLibrary.importUri == _internal;
}

/// Returns the class of the instance referred to by metadata annotation [node].
///
/// For example:
///
/// - `@JS()` would return the "JS" class in "dart:_js_annotations".
/// - `@anonymous` would return the "_Anonymous" class in
/// "dart:js_interop".
/// - `@staticInterop` would return the "_StaticInterop" class in
/// "dart:js_interop".
/// - `@Native` would return the "Native" class in "dart:_js_helper".
///
/// This function works regardless of whether the CFE is evaluating constants,
/// or whether the constant is a field reference (such as "anonymous" above).
Class? annotationClass(Expression node) {
  if (node is ConstantExpression) {
    var constant = node.constant;
    if (constant is InstanceConstant) return constant.classNode;
  } else if (node is ConstructorInvocation) {
    return node.target.enclosingClass;
  } else if (node is StaticGet) {
    var type = node.target.getterType;
    if (type is InterfaceType) return type.classNode;
  }
  return null;
}

/// Returns the string values inside of a metadata annotation [node].
///
/// For example:
/// - `@JS('Foo')` would return ['Foo'].
/// - `@Native('Foo,Bar')` would return ['Foo', 'Bar'].
///
/// [node] is expected to be an annotation with either StringConstants or
/// StringLiterals that can be made up of multiple values. If there are none,
/// this method returns an empty list. This method throws an assertion if there
/// are multiple arguments or a named arg in the annotation.
List<String> stringAnnotationValues(Expression node) {
  List<String> values = [];
  if (node is ConstantExpression) {
    var constant = node.constant;
    if (constant is InstanceConstant) {
      var argLength = constant.fieldValues.values.length;
      if (argLength == 1) {
        var value = constant.fieldValues.values.elementAt(0);
        if (value is StringConstant) values.addAll(value.value.split(','));
      } else if (argLength > 1) {
        throw ArgumentError(
          'Method expects annotation with at most one positional argument: '
          '$node.',
        );
      }
    }
  } else if (node is ConstructorInvocation) {
    var argLength = node.arguments.positional.length;
    if (argLength > 1 || node.arguments.named.isNotEmpty) {
      throw ArgumentError(
        'Method expects annotation with at most one positional argument: '
        '$node.',
      );
    } else if (argLength == 1) {
      var value = node.arguments.positional[0];
      if (value is StringLiteral) {
        values.addAll(value.value.split(','));
      } else if (value is StaticGet) {
        // Sometimes the CFE will translate the following to a StaticGet of a
        // const field:
        //
        // const String fieldName = 'field';
        // @JS(fieldName)
        //
        // In this case we derive the name from the intializer of the referenced
        // field.
        var target = value.target;
        if (target is Field && target.isConst) {
          final value = target.initializer;
          if (value is StringLiteral) {
            values.addAll(value.value.split(','));
          }
        }
      }
    }
  }
  return values;
}

/// Returns the [Library] within [libraries] matching the specified
/// [interopUri] or [null].
Library? _findJsInteropLibrary(List<Library> libraries, Uri interopUri) {
  for (Library lib in libraries) {
    for (LibraryDependency dependency in lib.dependencies) {
      Library targetLibrary = dependency.targetLibrary;
      if (targetLibrary.importUri == interopUri) {
        return targetLibrary;
      }
    }
  }
  return null;
}

/// Calculates the libraries in [component] that transitively import a given js
/// interop library.
///
/// NOTE: This function was based off of
/// `calculateTransitiveImportsOfDartFfiIfUsed` in
/// pkg/vm/lib/transformations/ffi/common.dart.
Set<Library> calculateTransitiveImportsOfJsInteropIfUsed(
  List<Library> libraries,
  Uri interopUri,
) {
  // Check for the presence of [jsInteropLibrary] as a dependency of any of the
  // libraries in [component]. We use this to bypass the expensive
  // [calculateTransitiveDependenciesOf] call for cases where js interop is
  // not used, otherwise we could just use the index of the library instead.
  Library? jsInteropLibrary = _findJsInteropLibrary(libraries, interopUri);
  if (jsInteropLibrary == null) return const <Library>{};

  kernel_graph.LibraryGraph graph = kernel_graph.LibraryGraph(libraries);
  Set<Library> result = kernel_graph.calculateTransitiveDependenciesOf(graph, {
    jsInteropLibrary,
  });
  return result;
}
